package visitor;

import symboltable.ClassTable;
import syntaxtree.*;
import symboltable.*;
import symboltable.Symbol;

public class TypeCheckVisitor implements TypeVisitor {

    private final SymbolTable symbolTable;
    private ClassTable currClass;
    private MethodTable currMethod;

    public TypeCheckVisitor(SymbolTable symboltable) {
        this.symbolTable = symboltable;
    }

    @Override
    public Type visit(Program n) {

        System.out.print("\n\nChecagem de tipos\n\n ");

        for (int i = 0; i < n.cl.size(); i++)
            n.cl.elementAt(i).accept(this);

        n.m.accept(this);
        System.out.print("\n\nFim da checagem de tipos\n\n");
        return null;
    }

    public boolean sameTypes(Type a, Type b) {
        if (!(a instanceof IntArrayType && b instanceof IntArrayType))
            if (!(a instanceof IntegerType && b instanceof IntegerType))
                if (!(a instanceof BooleanType && b instanceof BooleanType))
                    if (!(a instanceof IdentifierType && b instanceof IdentifierType))
                        return false;
                    else {
                        IdentifierType it1 = (IdentifierType) a;
                        IdentifierType it2 = (IdentifierType) b;
                        if (!(it1.s == null ? it2.s == null : it1.s.equals(it2.s)))
                            return false;
                    }

        return true;
    }

    @Override
    public Type visit(VarDecl n) {

        ErrorMsg error = new ErrorMsg();

        n.i.accept(this);
        Type t = n.t.accept(this);

        if (!(t instanceof BooleanType || t instanceof IntegerType || t instanceof IntArrayType))
            if (t instanceof IdentifierType) {
                IdentifierType it = (IdentifierType) t;
                if (!symbolTable.getSymbolTable().containsKey(Symbol.symbol(it.s)))
                    error.complain("O tipo para o qual " + n.i.s + " esta sendo declarado não foi encontrado.\n");
            }

        return null;
    }

    @Override
    public Type visit(If n) {
        Type t = n.e.accept(this);

        ErrorMsg error = new ErrorMsg();
        if (!(t instanceof BooleanType))
            error.complain("No if deve ter uma expressao do tipo booleano");

        n.s1.accept(this);
        n.s2.accept(this);

        return null;
    }

    @Override
    public Type visit(ClassDeclSimple n) {

        n.i.accept(this);

        currClass = symbolTable.getSymbolTable().get(Symbol.symbol((n.i.toString())));

        for (int i = 0; i < n.vl.size(); i++)
            n.vl.elementAt(i).accept(this);

        for (int i = 0; i < n.ml.size(); i++)
            n.ml.elementAt(i).accept(this);

        return null;
    }

    @Override
    public Type visit(MainClass n) {

        n.i1.accept(this);
        currClass = null;
        n.i2.accept(this);
        n.s.accept(this);
        return null;
    }

    @Override
    public Type visit(ClassDeclExtends n) {
        n.i.accept(this);
        currClass = symbolTable.getSymbolTable().get(Symbol.symbol((n.i.toString())));

        for (int i = 0; i < n.vdl.size(); i++)
            n.vdl.elementAt(i).accept(this);

        for (int i = 0; i < n.mdl.size(); i++)
            n.mdl.elementAt(i).accept(this);

        return null;
    }

    @Override
    public Type visit(MethodDecl n) {
        ErrorMsg error = new ErrorMsg();

        currMethod = currClass.getMethodsTable().getHashmapMethodTable().get(Symbol.symbol(n.i.s));

        Type te = n.e.accept(this);
        Type tt = n.t.accept(this);

        if (!sameTypes(te, tt))
            error.complain("\nvalor de retorno e retorno do método são incompativeis" + n.i.s + ".\n");

        for (int i = 0; i < n.fl.size(); i++)
            n.fl.elementAt(i).accept(this);

        for (int i = 0; i < n.vl.size(); i++)
            n.vl.elementAt(i).accept(this);

        for (int i = 0; i < n.sl.size(); i++)
            n.sl.elementAt(i).accept(this);

        return null;
    }

    @Override
    public Type visit(Formal n) {
        return null;
    }

    @Override
    public Type visit(IntArrayType n) {
        return n;
    }

    @Override
    public Type visit(Block n) {
        for (int i = 0; i < n.sl.size(); i = i + 1)
            n.sl.elementAt(i).accept(this);

        return null;
    }

    @Override
    public Type visit(While n) {

        Type t = n.e.accept(this);

        ErrorMsg error = new ErrorMsg();
        if (!(t instanceof BooleanType))
            error.complain("necessaria uma expressao do tipo booleano");

        n.s.accept(this);

        return null;
    }

    @Override
    public Type visit(Print n) {

        Type t = n.e.accept(this);

        ErrorMsg error = new ErrorMsg();

        if (!(t instanceof IntegerType))
            error.complain("Print somente para inteiros");

        return null;
    }

    @Override
    public Type visit(Assign n) {

        Type exp = n.e.accept(this);
        ErrorMsg error = new ErrorMsg();
        String erro = "atribuicao somente com expressoes do mesmo tipo";

        if (currMethod.getParamsTable().getHashmapParamsTable().containsKey(Symbol.symbol(n.i.toString()))) {
            if (!sameTypes(currMethod.getParamsTable().getHashmapParamsTable().get(Symbol.symbol(n.i.toString())), exp))
                error.complain(erro);

        } else
            if (currMethod.getLocalsTable().getHashmapLocalsTable().containsKey(Symbol.symbol(n.i.toString()))) {
                if (!sameTypes(currMethod.getLocalsTable().getHashmapLocalsTable().get(Symbol.symbol(n.i.toString())), exp))
                    error.complain(erro);

            } else

                if (currClass.getFieldsTable().getHashmapFieldsTable().containsKey(Symbol.symbol(n.i.toString()))) {
                    if (!sameTypes(currClass.getFieldsTable().getHashmapFieldsTable().get(Symbol.symbol(n.i.toString())), exp))
                        error.complain("atribuicao deve ser feita com expressoes do mesmo tipo");
                } else
                    error.complain("\nIdentificador " + Symbol.symbol(n.i.toString()) + " nao encontrado no escopo atual.\n");

        return null;
    }

    @Override
    public Type visit(ArrayAssign n) {

        Type exp1 = n.e1.accept(this);
        Type exp2 = n.e2.accept(this);

        ErrorMsg error = new ErrorMsg();

        if (!(exp1 instanceof IntegerType))
            error.complain("indice do vetor deve ser int");

        if (!(exp2 instanceof IntegerType))
            error.complain("exp do lado direito precisa ser int");

        return null;
    }

    @Override
    public Type visit(And n) {

        Type t1 = n.e1.accept(this);
        Type t2 = n.e2.accept(this);
        ErrorMsg error = new ErrorMsg();

        if (!((t1 instanceof BooleanType) && (t2 instanceof BooleanType)))
            error.complain("and deve estar entre dois bool");

        return new BooleanType();
    }

    @Override
    public Type visit(LessThan n) {

        Type t1 = n.e1.accept(this);
        Type t2 = n.e2.accept(this);
        ErrorMsg error = new ErrorMsg();

        if (!(t1 instanceof IntegerType && t2 instanceof IntegerType))
            error.complain("< deve estar entre dois int");

        return new BooleanType();
    }

    @Override
    public Type visit(BooleanType n) {
        return n;
    }

    @Override
    public Type visit(IntegerType n) {
        return n;
    }

    @Override
    public Type visit(IdentifierType n) {
        return n;
    }

    @Override
    public Type visit(Plus n) {
        Type t1 = n.e1.accept(this);
        Type t2 = n.e2.accept(this);
        ErrorMsg error = new ErrorMsg();

        if (!((t1 instanceof IntegerType) && (t2 instanceof IntegerType)))
            error.complain("+ deve estar entre dois int");

        return new IntegerType();
    }

    @Override
    public Type visit(Minus n) {

        Type t1 = n.e1.accept(this);
        Type t2 = n.e2.accept(this);
        ErrorMsg error = new ErrorMsg();

        if (!((t1 instanceof IntegerType) && (t2 instanceof IntegerType)))
            error.complain("- deve estar entre dois int");

        return new IntegerType();
    }

    @Override
    public Type visit(Times n) {

        Type t1 = n.e1.accept(this);
        Type t2 = n.e2.accept(this);
        ErrorMsg error = new ErrorMsg();

        if (!((t1 instanceof IntegerType) && (t2 instanceof IntegerType)))
            error.complain("* deve estar entre dois int");

        return new IntegerType();
    }

    @Override
    public Type visit(ArrayLookup n) {

        ErrorMsg error = new ErrorMsg();

        if (!(n.e2.accept(this) instanceof IntegerType))
            error.complain("lado direito do lenght deve ser um array");

        if (!(n.e1.accept(this) instanceof IntArrayType))
            error.complain("lado esquerdo do lenght deve estar dois");

        return new IntegerType();
    }

    @Override
    public Type visit(NewArray n) {
        ErrorMsg error = new ErrorMsg();
        Type t = n.e.accept(this);
        if (!(t instanceof IntegerType))
            error.complain("\nNa criacao de um Array deve ser passada uma exp do tipo inteiro.\n");

        return new IntArrayType();
    }

    @Override
    public Type visit(NewObject n) {

        return new IdentifierType(n.i.s);
    }

    @Override
    public Type visit(Call n) {
        ErrorMsg error = new ErrorMsg();

        Type tp = null;
        ClassTable auxClass;
        MethodTable auxMethod;

        Type te = n.e.accept(this);

        if (te instanceof IdentifierType) {

            IdentifierType ti = (IdentifierType) te;
            if (symbolTable.getSymbolTable().containsKey(Symbol.symbol(ti.s)))
                auxClass = symbolTable.getSymbolTable().get(Symbol.symbol(ti.s));
            else {
                error.complain("Classe " + ti.s + " nao foi encontrada.\n");
                return null;
            }

            if (auxClass.getMethodsTable().getHashmapMethodsTable().containsKey(Symbol.symbol(n.i.toString())))
                tp = auxClass.getMethodsTable().getHashmapMethodsTable().get(Symbol.symbol(n.i.toString()));
            else {
                error.complain("metodo " + n.i.s + " nao foi encontrado na Classe" + ti.s + ".\n");
                return null;
            }

        }

        return tp;
    }

    @Override
    public Type visit(Not n) {

        Type t = n.e.accept(this);
        ErrorMsg error = new ErrorMsg();

        if (!(t instanceof BooleanType))
            error.complain("! so deve ser feito para booleanos");

        return new BooleanType();
    }

    @Override
    public Type visit(ArrayLength n) {
        ErrorMsg error = new ErrorMsg();

        if (!(n.e.accept(this) instanceof IntArrayType))
            error.complain("lado direito do lenght deve ser um array int");

        return new IntegerType();
    }

    @Override
    public Type visit(IntegerLiteral n) {
        return new IntegerType();

    }

    @Override
    public Type visit(True n) {
        return new BooleanType();

    }

    @Override
    public Type visit(False n) {
        return new BooleanType();
    }

    @Override
    public Type visit(IdentifierExp n) {
        Type type = null;
        if (currMethod != null) {
            type = currMethod.getLocalsTable().getHashmapLocalsTable().get(Symbol.symbol(n.s));
            if (type == null)
                type = currMethod.getParamsTable().getHashmapParamsTable().get(Symbol.symbol(n.s));
        }
        if (type == null && currClass != null)
            type = currClass.getFieldsTable().getHashmapFieldsTable().get(Symbol.symbol(n.s));
        return type;
    }

    @Override
    public Type visit(This n) {
        return new IdentifierType(currClass.getIdentifier());
    }

    @Override
    public Type visit(Identifier n) {
        return null;
    }

}
